/*
  author Sylvain Bertrand <digital.ragnarok@gmail.com>
  Protected by GNU Affero GPL v3 with some exceptions.
  See README at root of alga tree.
*/

/*
 * NOTE: do not even try to understand this code without the displayport
 * specifications
 */
#include <linux/i2c.h>
#include <linux/delay.h>

#include <alga/alga.h>
#include <alga/pixel_fmts.h>
#include <alga/timing.h>
#include <alga/dp.h>

#include <alga/amd/atombios/atb.h>
#include <alga/amd/atombios/dce.h>
#include <alga/amd/dce4/dce4.h>
#include <alga/amd/dce4/dce4_dev.h>

#include "dce4.h"
#include "dpcd.h"

static char *vs_names(u8 vs_pre_emph)
{
	vs_pre_emph &= DPCD_VOLTAGE_SWING_MASK;

	switch (vs_pre_emph) {
	case DPCD_VOLTAGE_SWING_400:
		return "400 mV";
	case DPCD_VOLTAGE_SWING_600:
		return "600 mV";
	case DPCD_VOLTAGE_SWING_800:
		return "800 mV";
	case DPCD_VOLTAGE_SWING_1200:
		return "1200 mV";
	default:
		return "unregistered mV";
	}
}

static char *pre_emph_names(u8 vs_pre_emph)
{
	vs_pre_emph &= DPCD_PRE_EMPHASIS_MASK;

	switch (vs_pre_emph) {
	case DPCD_PRE_EMPHASIS_0:
		return "0 dB";
	case DPCD_PRE_EMPHASIS_3_5:
		return "3.5 dB";
	case DPCD_PRE_EMPHASIS_6:
		return "6 dB";
	case DPCD_PRE_EMPHASIS_9_5:
		return "9.5 dB";
	default:
		return "unregistered dB";
	}
}

static int tp_set(struct dce4 *dce, unsigned i, u8 tp)
{
	int r;

	dev_info(dce->ddev.dev, "dce4:dp%u:link training: setting training "
							"pattern %u\n", i, tp);
	r = atb_enc_dp_tp(dce->ddev.atb, i, dce->dps[i].link_rate,
						dce->dps[i].lanes_n, tp);
	if (r < 0){
		dev_err(dce->ddev.dev, "dce4:dp%u:link training: unable to set "
					"training pattern on encoder\n", i);
		return -DCE4_ERR;
	}

	r = dpcd_wr(dce, i, DPCD_TRAINING_PATTERN_SET, tp);
	if (r < 0) {
		dev_err(dce->ddev.dev, "dce4:dp%u:link training: unable to "
			"dpcd write the training pattern for dp sink\n", i);
		return r;
	}
	return 0;
}

static int vs_pre_emph_set(struct dce4 *dce, unsigned i, u8 vs_pre_emph)
{
	int r;

	dev_info(dce->ddev.dev, "dce4:dp%u:link training: setting vs=%s pre "
		"emph=%s at rate=%s\n", i, vs_names(vs_pre_emph),
				pre_emph_names(vs_pre_emph), link_rate_names(
							dce->dps[i].link_rate));
	/*
	 * set the initial vs/pre-emph on the dp source, all lanes at once, and
	 * there is only one value for the DCE transmitter.
	 */
	r = atb_trans_link_vs_pre_emph(dce->ddev.atb, i, vs_pre_emph);
	if (r < 0) {
		dev_err(dce->ddev.dev, "dce4:dp%u:link training: unable to set "
					"vs and pre-emph on transmitter\n", i);
		return -DCE4_ERR;
	}

	/*
	 * set the vs/pre-emph on the dp sink for all lanes in one auxiliary
	 * channel transaction, see dp specifications
	 */
	r = dpcd_lanes_vs_pre_emph_wr(dce, i, vs_pre_emph);
	if (r < 0)
		return r;
	return 0;
}

static u8 lane_status_extract(u8 *link_status, unsigned l)
{
	u8 lanes_pair_status;

	lanes_pair_status = link_status[l >> 1];/* lanes 0,1->0
						   lanes 2,3->1 */
	if ((l & 1) != 0)			/* 0,2->lsbs, 1,3->msbs */
		lanes_pair_status = lanes_pair_status >> 4;
	return lanes_pair_status & 0xf;
}

static bool clock_recovered(struct dce4 *dce, unsigned i, u8 *link_status)
{
	unsigned l;

	for (l = 0; l < dce->dps[i].lanes_n; ++l) {
		u8 lane_status;

		lane_status = lane_status_extract(link_status, l);
		if ((lane_status & DPCD_LANE_CR_DONE) == 0)
			return false;
	}
	return true;
}

static int link_rate_downshift(struct dce4 *dce, unsigned i)
{
	switch (dce->dps[i].link_rate) {
	case GHZ_1_62:
		return -DCE4_ERR;
	case GHZ_2_7:
		dce->dps[i].link_rate = GHZ_1_62;
		break;
	case GHZ_5_4:
		dce->dps[i].link_rate = GHZ_2_7;
		break;
	}
	dev_info(dce->ddev.dev, "dce4:dp%u:link training: downshifting link "
		"rate to %s\n", i, link_rate_names(dce->dps[i].link_rate));
	return dpcd_wr(dce, i, DPCD_LINK_BW_SET, dce->dps[i].link_rate);
}

static void lane_adjust_req_extract(u8 *link_status, unsigned l, u8 *vs,
								u8 *pre_emph)
{
	u8 vs_pre_emph;

	vs_pre_emph = link_status[DPCD_ADJUST_REQUEST_LANE0_1
					- DPCD_LANE0_1_STATUS + (l >> 1)];
	if (l & 1)
		vs_pre_emph = vs_pre_emph >>
					DPCD_ADJUST_VOLTAGE_SWING_LANEY_SHIFT;

	*vs = vs_pre_emph & DPCD_ADJUST_VOLTAGE_SWING_LANEX_MASK;
	*pre_emph = vs_pre_emph & DPCD_ADJUST_PRE_EMPHASIS_LANEX_MASK;
}

/* those return values are significant only for the clock recovery sequence */
#define VS_DIFFERENT	1
#define VS_SAME		0
static int drive_adjust(struct dce4 *dce, unsigned i, u8 *vs_pre_emph,
								u8 *link_status)
{
	int r;
	unsigned l;
	u8 current_vs;
	/*
	 * only one vs and pre emph (highest values) for all lanes in DCE dp
	 * transmitter
	 */
	u8 vs = 0;
	u8 pre_emph = 0;

	r = VS_SAME;

	/* get the adjust request highest values */
	for (l = 0; l < dce->dps[i].lanes_n; ++l) {
		u8 adjust_req_vs;
		u8 adjust_req_pre_emph;

		lane_adjust_req_extract(link_status, l, &adjust_req_vs,
							&adjust_req_pre_emph);

		if (adjust_req_vs > vs)
			vs = adjust_req_vs;
		if (adjust_req_pre_emph > pre_emph)
			pre_emph = adjust_req_pre_emph;
	}

	pre_emph <<= 1;	/* shift request bits for lane set register */

	current_vs = *vs_pre_emph & DPCD_VOLTAGE_SWING_MASK;
	if (vs != current_vs)
		r = VS_DIFFERENT;

	if (vs >= DPCD_VOLTAGE_SWING_1200)
		vs |= DPCD_MAX_VOLTAGE_SWING_REACHED;
	if (pre_emph >= DPCD_PRE_EMPHASIS_9_5)
		pre_emph |= DPCD_MAX_PRE_EMPHASIS_REACHED;

	*vs_pre_emph = pre_emph | vs;
	return r;
}

#define CLOCK_RECOVERY_LINK_RATE_DOWNSHIFT_REQUEST 1
static int clock_recovery(struct dce4 *dce, unsigned i, u8 *vs_pre_emph)
{
	int r;
	u8 link_status[DPCD_LINK_STATUS_SZ];
	unsigned same_vs_loop_count;

	r = tp_set(dce, i, 1);
	if (r < 0)
		return r;

	*vs_pre_emph = 0;
	r = vs_pre_emph_set(dce, i, *vs_pre_emph);
	if (r < 0)
		return r;
	
	/* be sure the aux chan is ready, waiting for its timeout (dp specs) */
	udelay(400);

	same_vs_loop_count = 1;	
	while (1) {
		dev_info(dce->ddev.dev, "dce4:dp%u:link training: clock"
			" recovery test %u at %s\n", i, same_vs_loop_count,
							vs_names(*vs_pre_emph));

		udelay(100);/* from dp 1.2, dpcd provides this value */

		r = dpcd_link_status(dce, i, link_status);	
		if (r < 0)
			return r;

		if (clock_recovered(dce, i, link_status))
			break;

		if (same_vs_loop_count >= 5 || ((*vs_pre_emph
					& DPCD_MAX_VOLTAGE_SWING_REACHED) != 0))
			return CLOCK_RECOVERY_LINK_RATE_DOWNSHIFT_REQUEST;

		r = drive_adjust(dce, i, vs_pre_emph, link_status);
		if (r == VS_SAME)
			++same_vs_loop_count;
		else if (r == VS_DIFFERENT)
			same_vs_loop_count = 1;

		r = vs_pre_emph_set(dce, i, *vs_pre_emph);
		if (r < 0)
			return r;
	}
	dev_info(dce->ddev.dev, "dce4:dp%u:link training: clock recovery "
		"successful for vs=%s pre emph=%s at rate=%s, %u lanes\n", i,
		vs_names(*vs_pre_emph), pre_emph_names(*vs_pre_emph),
		link_rate_names(dce->dps[i].link_rate), dce->dps[i].lanes_n);
	return 0;
}

static int clock_recovery_link_rate_loop(struct dce4 *dce, unsigned i,
							u8 *vs_pre_emph)
{
	int r;

	while (1) {
		r = clock_recovery(dce, i, vs_pre_emph);
		if (r < 0)
			return r;
		if (r == 0)
			break;

		r = link_rate_downshift(dce, i);
		if (r < 0)
			return r;
	}
	return r;
}

static int train_init(struct dce4 *dce, unsigned i)
{
	int r;
	u8 tmp;

	/* be sure the sink is in active power state */
	r = dpcd_wr(dce, i, DPCD_SET_PWR, DPCD_SET_PWR_D0);
	if (r < 0)
		return r;

	/* enable dp downspread */
	if ((dce->dps[i].dpcd_info[DPCD_MAX_DOWNSPREAD]
					& DPCD_MAX_DOWNSPREAD_SUPPORT) != 0) {
		r = dpcd_wr(dce, i, DPCD_DOWNSPREAD_CTL, DPCD_SPREAD_AMP_0_5);
		if (r < 0)
			return r;
	}

	/* get the maximum lanes we can get */
	tmp = (u8)(dce->dps[i].lanes_n);
	if ((dce->dps[i].dpcd_info[DPCD_MAX_LANE_COUNT]
				& DPCD_LANE_COUNT_ENHANCED_FRAME_EN) != 0)
		tmp |= DPCD_LANE_COUNT_ENHANCED_FRAME_EN;

	r = dpcd_wr(dce, i, DPCD_LANE_COUNT_SET, tmp);
	if (r < 0)
		return r;

	/* start with the best link rate we can have */
	r = dpcd_wr(dce, i, DPCD_LINK_BW_SET, dce->dps[i].link_rate);
	if (r < 0)
		return r;

	/* tell the digital encoder we are about to emit training patterns */
	r = atb_enc_dp_training_start(dce->ddev.atb, i);
	if (r < 0)
		return -DCE4_ERR;

	/* be sure the sink is not in training state */
	r = dpcd_wr(dce, i, DPCD_TRAINING_PATTERN_SET,
						DPCD_TRAINING_PATTERN_DISABLE);
	if (r < 0)
		return r;
	return 0;
}

static bool channel_equalized(struct dce4 *dce, unsigned i, u8 *link_status)
{
	u8 align_status;
	unsigned l;

	align_status = link_status[DPCD_LANE_ALIGN_STATUS_UPDATED
							- DPCD_LANE0_1_STATUS];

	if ((align_status & DPCD_INTERLANE_ALIGN_DONE) == 0)
		return false;

	for (l = 0; l < dce->dps[i].lanes_n; ++l) {
		u8 lane_status;
		lane_status = lane_status_extract(link_status, l);	

		if (((lane_status & DPCD_LANE_CHANNEL_EQ_DONE) == 0)
			|| ((lane_status & DPCD_LANE_SYMBOL_LOCKED) == 0))
			return false;
	}
	return true;
}

#define CLOCK_RECOVERY_REQUEST 1
static int channel_equalization(struct dce4 *dce, unsigned i, u8 *vs_pre_emph)
{
	int r;
	unsigned loop_count;
	u8 link_status[DPCD_LINK_STATUS_SZ];

	r = tp_set(dce, i, 2); /* from dp 1.2, there is a 3rd pattern */
	if (r < 0)
		return r;

	r = vs_pre_emph_set(dce, i, *vs_pre_emph);
	if (r < 0)
		return r;

	loop_count = 1;
	while (1) {
		dev_info(dce->ddev.dev, "dce4:dp%u:link training: channel"
				" equalization test %u\n", i, loop_count);

		udelay(400);

		r = dpcd_link_status(dce, i, link_status);	
		if (r < 0)
			return r;

		if (!clock_recovered(dce, i, link_status)) {
			r = link_rate_downshift(dce, i);
			if (r < 0)
				return r;
			return CLOCK_RECOVERY_REQUEST;
		}

		if (channel_equalized(dce, i, link_status))
			break;

		if (loop_count > 5) {
			r = link_rate_downshift(dce, i);
			if (r < 0)
				return r;
			return CLOCK_RECOVERY_REQUEST;
		}

		drive_adjust(dce, i, vs_pre_emph, link_status);

		r = vs_pre_emph_set(dce, i, *vs_pre_emph);
		if (r < 0)
			return r;

		++loop_count;
	}
	dev_info(dce->ddev.dev, "dce4:dp%u:link training: channel equalization "
		"successful for vs=%s pre emph=%s at rate=%s, %u lanes\n", i,
		vs_names(*vs_pre_emph), pre_emph_names(*vs_pre_emph),
		link_rate_names(dce->dps[i].link_rate), dce->dps[i].lanes_n);
	return 0;
}

static int train_finish(struct dce4 *dce, unsigned i)
{
	int r;

	udelay(400);

	r = dpcd_wr(dce, i, DPCD_TRAINING_PATTERN_SET, 0);
	if (r < 0) {
		dev_err(dce->ddev.dev, "dce4:dp%u:link training: unable to "
			"dpcd disable the training pattern for dp sink\n", i);
		return r;
	}

	r = atb_enc_dp_training_complete(dce->ddev.atb, i);
	if (r < 0) {
		dev_err(dce->ddev.dev, "dce4:dp%u:link training: unable to "
				"disable the training pattern on encoder\n", i);
		return -DCE4_ERR;
	}
	return 0;
}

/* bw is the amount of MB per second we want the link to transfer */
int dp_link_train(struct dce4 *dce, unsigned i)
{
	int r;
	u8 vs_pre_emph;

	r = train_init(dce, i);
	if (r < 0) {
		dev_err(dce->ddev.dev, "dce4:dp%u:link training: init failed\n",
									i);
		return r;
	}

	while (1) {
		r = clock_recovery_link_rate_loop(dce, i, &vs_pre_emph);
		if (r < 0)
			return r;

		r = channel_equalization(dce, i, &vs_pre_emph);
		if (r < 0)
			return r;
		if (r == 0)
			break;
	}
	if (r < 0)
		dev_err(dce->ddev.dev, "dce4:dp%u:link training: failure, "
			"last attempt is vs=%s pre emph=%s at rate=%s\n", i,
			vs_names(vs_pre_emph), pre_emph_names(vs_pre_emph),
					link_rate_names(dce->dps[i].link_rate));
	r = train_finish(dce, i);
	return r;
}
